<?php

namespace support\coze;

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\MultipartStream;
use GuzzleHttp\RequestOptions;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class OauthClient
{
    private static $instance;

    private $appId;
    private $privateKeyPath;  // 改为私钥文件路径
    private $publicKeyId;
    private $accessToken;
    private $tokenExpireTime;
    private $debug = false;  // 调试开关
    private $tokenLock = false;  // 防止并发获取token的锁

    const BASE_URL = 'https://api.coze.cn';
    const TOKEN_ENDPOINT = '/api/permission/oauth2/token';
    const AUDIENCE = 'api.coze.cn';

    private function __construct($appId, $privateKeyPath, $publicKeyId)
    {
        $this->appId = $appId;
        $this->privateKeyPath = $privateKeyPath;
        $this->publicKeyId = $publicKeyId;
    }

    public static function getInstance($appId, $privateKeyPath, $publicKeyId)
    {
        if (is_null(self::$instance)) {
            self::$instance = new self($appId, $privateKeyPath, $publicKeyId);
        }

        return self::$instance;
    }

    /**
     * Enable or disable debug mode
     * 
     * @param bool $debug Whether to enable debug output
     */
    public function setDebug($debug = true)
    {
        $this->debug = $debug;
    }

    /**
     * Log debug information to console
     * 
     * @param string $title Debug title
     * @param mixed $data Data to log
     */
    private function debugLog($title, $data = null)
    {
        if (!$this->debug) {
            return;
        }

        echo "\n" . str_repeat('=', 50) . "\n";
        echo "[DEBUG] {$title}\n";
        echo str_repeat('-', 50) . "\n";

        if ($data !== null) {
            if (is_array($data) || is_object($data)) {
                echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
            } else {
                echo $data . "\n";
            }
        }

        echo str_repeat('=', 50) . "\n";
    }

    /**
     * Generate JWT token for OAuth authentication
     * 
     * @param int $expiredSeconds JWT token expiration time in seconds (default 600)
     * @param string|null $sessionName Session identifier for user separation
     * @param array|null $sessionContext Session context information
     * @return string JWT token
     */
    private function generateJWT($expiredSeconds = 600, $sessionName = null, $sessionContext = null)
    {
        // 读取私钥文件
        if (!file_exists($this->privateKeyPath)) {
            throw new \Exception("Private key file not found: " . $this->privateKeyPath);
        }

        $privateKey = file_get_contents($this->privateKeyPath);
        if (!$privateKey) {
            throw new \Exception("Unable to read private key file");
        }

        $now = time();

        // JWT Payload
        $payload = [
            'iss' => $this->appId,                    // OAuth App ID
            'aud' => self::AUDIENCE,                  // Coze API endpoint
            'iat' => $now,                            // Issued at time
            'exp' => $now + $expiredSeconds,          // Expiration time
            'jti' => $this->generateJti(),            // Unique identifier to prevent replay attacks
        ];

        // Optional session information
        if ($sessionName) {
            $payload['session_name'] = $sessionName;
        }

        if ($sessionContext) {
            $payload['session_context'] = $sessionContext;
        }

        // Set JWT header with key ID
        $headers = [
            'kid' => $this->publicKeyId,
            'typ' => 'JWT',
            'alg' => 'RS256'
        ];

        $this->debugLog('JWT Payload', $payload);
        $this->debugLog('JWT Headers', $headers);

        try {
            $jwt = JWT::encode($payload, $privateKey, 'RS256', null, $headers);
            $this->debugLog('Generated JWT', $jwt);
            return $jwt;
        } catch (\Exception $e) {
            throw new \Exception('Unable to generate JWT token: ' . $e->getMessage());
        }
    }

    /**
     * Generate unique identifier for JWT to prevent replay attacks
     * 
     * @return string Random string with length > 32 bytes
     */
    private function generateJti()
    {
        return bin2hex(random_bytes(20)) . uniqid() . microtime(true);
    }

    /**
     * Get OAuth access token using JWT with automatic refresh
     * 
     * @param int $durationSeconds Access token duration in seconds (max 86399)
     * @param string|null $sessionName Session identifier
     * @param array|null $sessionContext Session context
     * @param bool $forceRefresh Force refresh token even if current token is valid
     * @return string Access token
     * @throws \Exception
     */
    public function getAccessToken($durationSeconds = 900, $sessionName = null, $sessionContext = null, $forceRefresh = false)
    {
        // 如果强制刷新，直接跳过缓存检查
        if (!$forceRefresh) {
            // Check if current token is still valid (提前2分钟刷新，避免在使用过程中过期)
            if ($this->accessToken && $this->tokenExpireTime && time() < $this->tokenExpireTime - 120) {
                $this->debugLog('Using cached access token', [
                    'token' => substr($this->accessToken, 0, 20) . '...',
                    'expires_at' => date('Y-m-d H:i:s', $this->tokenExpireTime),
                    'remaining_seconds' => $this->tokenExpireTime - time()
                ]);
                return $this->accessToken;
            }
        }

        // 防止并发获取token
        if ($this->tokenLock) {
            $this->debugLog('Waiting for token refresh to complete...');
            // 等待其他进程完成token获取
            $maxWaitTime = 30; // 最多等待30秒
            $waited = 0;
            while ($this->tokenLock && $waited < $maxWaitTime) {
                usleep(500000); // 等待0.5秒
                $waited += 0.5;
            }

            // 如果等待后有有效token，直接返回
            if ($this->accessToken && $this->tokenExpireTime && time() < $this->tokenExpireTime - 60) {
                return $this->accessToken;
            }
        }

        // 设置锁，防止并发获取
        $this->tokenLock = true;

        try {
            $this->debugLog('Refreshing access token', [
                'current_token_expired' => $this->tokenExpireTime ? date('Y-m-d H:i:s', $this->tokenExpireTime) : 'No token',
                'force_refresh' => $forceRefresh
            ]);

            // Generate JWT for authentication
            $jwt = $this->generateJWT(600, $sessionName, $sessionContext);

            // Create HTTP client for token request
            $client = new Client([
                'base_uri' => self::BASE_URL,
                'timeout' => 30,  // 设置超时时间
                'connect_timeout' => 10
            ]);

            $body = json_encode([
                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                'assertion' => $jwt,
                'duration_seconds' => min($durationSeconds, 86399) // Max 24 hours
            ]);

            $headers = [
                'Content-Type' => 'application/json',
                'Authorization' => 'Bearer ' . $jwt
            ];

            $this->debugLog('Token Request URL', self::BASE_URL . self::TOKEN_ENDPOINT);
            $this->debugLog('Token Request Headers', $headers);
            $this->debugLog('Token Request Body', json_decode($body, true));

            $response = $client->post(self::TOKEN_ENDPOINT, [
                'body' => $body,
                'headers' => $headers
            ]);

            $responseBody = $response->getBody()->getContents();
            $data = json_decode($responseBody, true);

            $this->debugLog('Token Response Status', $response->getStatusCode());
            $this->debugLog('Token Response Headers', $response->getHeaders());
            $this->debugLog('Token Response Body', $data);

            if (isset($data['access_token'])) {
                $this->accessToken = $data['access_token'];
                $this->tokenExpireTime = time() + $data['expires_in'];

                $this->debugLog('Token refreshed successfully', [
                    'expires_at' => date('Y-m-d H:i:s', $this->tokenExpireTime),
                    'expires_in' => $data['expires_in'] . ' seconds'
                ]);

                return $this->accessToken;
            } else {
                throw new \Exception('Failed to get access token: ' . json_encode($data));
            }
        } catch (\Exception $e) {
            $this->debugLog('Token refresh failed', $e->getMessage());
            throw $e;
        } finally {
            // 释放锁
            $this->tokenLock = false;
        }
    }

    /**
     * Get HTTP client with OAuth authentication and automatic token refresh
     * 
     * @param string|null $sessionName Session identifier
     * @param array|null $sessionContext Session context
     * @return Client
     */
    public function getClient($sessionName = null, $sessionContext = null)
    {
        $accessToken = $this->getAccessToken(900, $sessionName, $sessionContext);

        $headers = [
            'Authorization' => 'Bearer ' . $accessToken,
            'Content-Type' => 'application/json',
        ];

        $this->debugLog('Client Headers', $headers);

        return new Client([
            'base_uri' => self::BASE_URL,
            'headers' => $headers,
            'timeout' => 60,  // 设置请求超时时间
            'connect_timeout' => 10
        ]);
    }

    /**
     * Execute API request with automatic token refresh on 401 errors
     * 
     * @param callable $requestCallback Callback function that makes the API request
     * @param string|null $sessionName Session identifier
     * @param array|null $sessionContext Session context
     * @param int $maxRetries Maximum number of retry attempts
     * @return mixed Request result
     * @throws \Exception
     */
    public function executeWithRetry($requestCallback, $sessionName = null, $sessionContext = null, $maxRetries = 2)
    {
        $attempt = 0;

        while ($attempt < $maxRetries) {
            try {
                $client = $this->getClient($sessionName, $sessionContext);
                return $requestCallback($client);
            } catch (\GuzzleHttp\Exception\ClientException $e) {
                $statusCode = $e->getResponse()->getStatusCode();

                if ($statusCode === 401 && $attempt < $maxRetries - 1) {
                    $this->debugLog('Received 401 error, refreshing token', [
                        'attempt' => $attempt + 1,
                        'max_retries' => $maxRetries
                    ]);

                    // 强制刷新token
                    $this->getAccessToken(900, $sessionName, $sessionContext, true);
                    $attempt++;
                    continue;
                }

                throw $e;
            }
        }

        throw new \Exception('Max retry attempts reached');
    }

    /**
     * Upload file to Coze platform with automatic retry
     * 
     * @param string $file File path
     * @param string|null $sessionName Session identifier
     * @param array|null $sessionContext Session context
     * @return array Upload result
     * @throws \Exception
     */
    public function uploadFile($file, $sessionName = null, $sessionContext = null)
    {
        return $this->executeWithRetry(function ($client) use ($file) {
            $multipart = [
                [
                    'name' => 'file',
                    'contents' => fopen($file, 'r'),
                    'filename' => basename($file)
                ],
            ];

            $body = new MultipartStream($multipart);

            $headers = [
                'Content-Type' => 'multipart/form-data; boundary=' . $body->getBoundary()
            ];

            $this->debugLog('Upload File Request URL', self::BASE_URL . '/v1/files/upload');
            $this->debugLog('Upload File Info', [
                'file_path' => $file,
                'file_name' => basename($file),
                'file_size' => file_exists($file) ? filesize($file) : 'File not found'
            ]);

            $res = $client->post('/v1/files/upload', [
                'body' => $body,
                'headers' => array_merge($client->getConfig('headers'), $headers),
            ]);

            $responseBody = $res->getBody()->getContents();
            $data = json_decode($responseBody, true);

            $this->debugLog('Upload Response Status', $res->getStatusCode());
            $this->debugLog('Upload Response Body', $data);

            if ($data['code'] == 0) {
                return $data['data'];
            } else {
                throw new \Exception($data['msg']);
            }
        }, $sessionName, $sessionContext);
    }

    /**
     * Run workflow asynchronously with automatic retry
     * 
     * @param string $workFlowId Workflow ID
     * @param array $params Workflow parameters
     * @param string|null $sessionName Session identifier
     * @param array|null $sessionContext Session context
     * @return array Execution result
     * @throws \Exception
     */
    public function asyncRunWorkFlow($workFlowId, $params, $sessionName = null, $sessionContext = null)
    {
        return $this->executeWithRetry(function ($client) use ($workFlowId, $params) {
            $requestData = [
                'workflow_id' => $workFlowId,
                'parameters' => $params,
                'is_async' => true,
            ];

            $body = json_encode($requestData);

            $this->debugLog('Async Workflow Request URL', self::BASE_URL . '/v1/workflow/run');
            $this->debugLog('Async Workflow Request Data', $requestData);

            $res = $client->post('/v1/workflow/run', [
                'body' => $body,
            ]);

            $responseBody = $res->getBody()->getContents();
            $data = json_decode($responseBody, true);

            $this->debugLog('Async Workflow Response Status', $res->getStatusCode());
            $this->debugLog('Async Workflow Response Body', $data);

            if ($data['code'] == 0) {
                return $data;
            } else {
                throw new \Exception($data['msg']);
            }
        }, $sessionName, $sessionContext);
    }

    /**
     * Run workflow synchronously with automatic retry
     * 
     * @param string $workFlowId Workflow ID
     * @param array $params Workflow parameters
     * @param string|null $sessionName Session identifier
     * @param array|null $sessionContext Session context
     * @return array Execution result
     * @throws \Exception
     */
    public function runWorkFlow($workFlowId, $params, $sessionName = null, $sessionContext = null)
    {
        return $this->executeWithRetry(function ($client) use ($workFlowId, $params) {
            $requestData = [
                'workflow_id' => $workFlowId,
                'parameters' => $params,
            ];

            $body = json_encode($requestData);

            $this->debugLog('Sync Workflow Request URL', self::BASE_URL . '/v1/workflow/run');
            $this->debugLog('Sync Workflow Request Data', $requestData);

            $res = $client->post('/v1/workflow/run', [
                'body' => $body,
            ]);

            $responseBody = $res->getBody()->getContents();
            $data = json_decode($responseBody, true);

            $this->debugLog('Sync Workflow Response Status', $res->getStatusCode());
            $this->debugLog('Sync Workflow Response Body', $data);

            if ($data['code'] == 0) {
                return $data;
            } else {
                throw new \Exception($data['msg']);
            }
        }, $sessionName, $sessionContext);
    }

    /**
     * Query workflow execution status with automatic retry
     * 
     * @param string $workFlowId Workflow ID
     * @param string $excuteId Execution ID
     * @param string|null $sessionName Session identifier
     * @param array|null $sessionContext Session context
     * @return array Execution status
     * @throws \Exception
     */
    public function queryRunWorkFlow($workFlowId, $excuteId, $sessionName = null, $sessionContext = null)
    {
        return $this->executeWithRetry(function ($client) use ($workFlowId, $excuteId) {
            $url = "/v1/workflows/{$workFlowId}/run_histories/{$excuteId}";

            $this->debugLog('Query Workflow Request URL', self::BASE_URL . $url);
            $this->debugLog('Query Parameters', [
                'workflow_id' => $workFlowId,
                'execute_id' => $excuteId
            ]);

            $res = $client->get($url);

            $responseBody = $res->getBody()->getContents();
            $body = json_decode($responseBody, true);

            $this->debugLog('Query Workflow Response Status', $res->getStatusCode());
            $this->debugLog('Query Workflow Response Body', $body);

            if ($body['code'] == 0) {
                return $body;
            } else {
                throw new \Exception($body['msg']);
            }
        }, $sessionName, $sessionContext);
    }

    /**
     * Create session context for device information
     * 
     * @param string|null $deviceId IoT device ID
     * @param string|null $customConsumer Custom consumer ID
     * @return array Session context
     */
    public static function createSessionContext($deviceId = null, $customConsumer = null)
    {
        $context = [];

        if ($deviceId || $customConsumer) {
            $deviceInfo = [];

            if ($deviceId) {
                $deviceInfo['device_id'] = $deviceId;
            }

            if ($customConsumer) {
                $deviceInfo['custom_consumer'] = $customConsumer;
            }

            $context['device_info'] = $deviceInfo;
        }

        return $context;
    }

    /**
     * Check if current token is valid
     * 
     * @param int $bufferSeconds Buffer time in seconds before expiration
     * @return bool True if token is valid
     */
    public function isTokenValid($bufferSeconds = 120)
    {
        return $this->accessToken &&
            $this->tokenExpireTime &&
            time() < $this->tokenExpireTime - $bufferSeconds;
    }

    /**
     * Get token expiration info
     * 
     * @return array Token expiration information
     */
    public function getTokenInfo()
    {
        if (!$this->accessToken) {
            return ['has_token' => false];
        }

        return [
            'has_token' => true,
            'expires_at' => date('Y-m-d H:i:s', $this->tokenExpireTime),
            'expires_in_seconds' => $this->tokenExpireTime - time(),
            'is_valid' => $this->isTokenValid()
        ];
    }

    /**
     * Set new OAuth credentials (useful for testing or credential rotation)
     * 
     * @param string $appId OAuth App ID
     * @param string $privateKey Private key content
     * @param string $publicKeyId Public key fingerprint
     */
    public function setCredentials($appId, $privateKey, $publicKeyId)
    {
        $this->appId = $appId;
        $this->privateKey = $privateKey;
        $this->publicKeyId = $publicKeyId;

        // Clear cached token when credentials change
        $this->accessToken = null;
        $this->tokenExpireTime = null;
    }
}